package it.fflabs.dynscript.mapsexample.client;

import it.fflabs.dynscript.client.JsClient;
import it.fflabs.dynscript.client.handler.IJsCallBack;
import it.fflabs.dynscript.client.handler.JsHandler;
import it.fflabs.dynscript.client.utils.JsArrayUtil;
import it.fflabs.dynscript.client.validation.IJsValidator;
import it.fflabs.dynscript.client.validation.exception.JsValidationException;
import it.fflabs.dynscript.mapsexample.client.model.INGVResult;
import it.fflabs.dynscript.mapsexample.client.model.TwitterBean;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.maps.client.InfoWindowContent;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.Maps;
import com.google.gwt.maps.client.control.LargeMapControl;
import com.google.gwt.maps.client.event.MarkerClickHandler;
import com.google.gwt.maps.client.geom.LatLng;
import com.google.gwt.maps.client.overlay.Marker;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.SimplePanel;

/**
 * <b>gwt-dynScript-mapsExample : Gwt_dynScript_mapsExample</b><br>
 * <hr>
 * Comment: taken from
 * http://code.google.com/p/gwt-google-apis/wiki/MapsGettingStarted<br>
 * <hr>
 * 18/feb/2012<br>
 * 
 * @author Francesco Foresti
 * 
 */
public class Gwt_dynScript_mapsExample implements EntryPoint {

	private static final int INFO_WINDOW_AUTOCLOSE = 5000;
	protected static final int RESULTS = 50;
	protected static final int UPDATESEVERYMS = 10*1000*60; // 10 minutes

	private Date now = new Date();
	private List<INGVResult> onScreenResults = new ArrayList<INGVResult>();
	private Label head;
	private MapWidget map = null;
	SimplePanel panel = null;

	/**
	 * This is the entry point method.
	 */
	public void onModuleLoad() {
		/*
		 * Asynchronously loads the Maps API.
		 * 
		 * The first parameter should be a valid Maps API Key to deploy this
		 * application on a public server, but a blank key will work for an
		 * application served from localhost.
		 */
		Maps.loadMapsApi("", "2", false, new Runnable() {
			public void run() {

				head = new Label();
				RootPanel.get().add(head);
				head.getElement().setAttribute("style", "margin-bottom:2%;");

				// second step : get some recent earthquakes via tweets from the INGVTerremoti user 
				final JsHandler twitterHandler = new JsHandler(
						new IJsCallBack() {

							// if the twitter call succeeds, this is the invoked method.
							public void onSuccess(JavaScriptObject jso) {
								now.setTime(System.currentTimeMillis());
								head.setText("Recent earthquakes detected by the INGV (Istituto Nazionale Geofisica e Vulcanologia), Italy. Last updated : "
										+ now.toString());

								TwitterBean twitter = TwitterBean.as(jso);
								JavaScriptObject results = twitter.getResults();
								@SuppressWarnings("unchecked")
								final INGVResult[] earthquakes = JsArrayUtil
										.unwrapArray(
												(JsArray<INGVResult>) results,
												new INGVResult[RESULTS]);
								if(!thereAreUpdates(earthquakes,onScreenResults)) {
									return;
								}
								final LatLng center = calculateMapCenter(earthquakes);
								if(map == null) {
									buildUi(center);
								}
								updateUI(earthquakes);
							}

							// if there is a timeout (no response) or something goes wrong along the way,
							// this is the method invoked.
							public void onTimeout() {
								Window.alert("the request timed out!");
							}
						}, new IJsValidator() {

							// validation of the received content. 
							public void validate(JavaScriptObject jso)
									throws JsValidationException {
								if (jso == null)
									throw new JsValidationException(
											"received null content");
							}
						});

				String twitterUrl = "http://search.twitter.com/search.json?q=from%3AINGVterremoti&rpp="
						+ RESULTS + "&result_type=recent";
				JsClient.doGet(twitterUrl, twitterHandler, 10000);
				head.setText("Twitter search started...");

				// schedule map updates.
				Timer t = new Timer() {

					@Override
					public void run() {
						String twitterUrl = "http://search.twitter.com/search.json?q=from%3AINGVterremoti&rpp="
								+ RESULTS + "&result_type=recent";
						JsClient.doGet(twitterUrl, twitterHandler, 10000);
						head.setText("updating via Twitter");
					}
				};
				t.scheduleRepeating(UPDATESEVERYMS);

			}
		});

	}

	/**
	 * Adds the given array of {@link INGVResult} on the map as markers.
	 * An info popup is also added to the most recent result.
	 * @param earthquakes
	 */
	private void updateUI(INGVResult[] earthquakes) {
		map.clearOverlays();
		onScreenResults.clear();
		// Add the markers
		for (final INGVResult earthquake : earthquakes) {

			Marker marker = addEarthquake(earthquake, map, onScreenResults);

			// Add an info window to highlight a point of interest :
			// assuming tweets are reverse-time-ordered (freshest first), 
			// the first element in the onScreen list is the most 
			// recent earthquake happened.
			if (earthquake.equals(onScreenResults.get(0))) {
				map.getInfoWindow().open(
						marker.getLatLng(),
						new InfoWindowContent("Most recent earthquake : "
								+ earthquake.getText()));
			}
		}
		
	}

	/**
	 * Creates a {@link MapWidget} with the specified center and
	 * some q&d css styling.
	 * @param center
	 */
	private void buildUi(LatLng center) {

		// sets the zoom level to roughly fit europe
		map = new MapWidget(center, 5);
		map.setSize("100%", "100%");

		// Add some controls for the zoom level
		map.addControl(new LargeMapControl());

		SimplePanel panel = new SimplePanel();
		panel.add(map);
		panel.getElement().setAttribute("style",
				"height: 700px; width:1000px; float: left; margin-left:10%;");
		// Add the map to the HTML host page
		RootPanel.get().add(panel);
	}

	/**
	 * @param earthquake
	 * @param map
	 * @param onScreenResults
	 * @return a {@link Marker} for the given {@link INGVResult}
	 */
	private Marker addEarthquake(final INGVResult earthquake,
			final MapWidget map, List<INGVResult> onScreenResults) {
		LatLng eQuakeCoords = LatLng.newInstance(earthquake.getLatitude(),
				earthquake.getLongitude());
		Marker marker = new Marker(eQuakeCoords);
		marker.setVisible(true);
		marker.addMarkerClickHandler(new MarkerClickHandler() {

			public void onClick(MarkerClickEvent event) {
				map.getInfoWindow().open(event.getSender().getLatLng(),
						new InfoWindowContent(earthquake.getText()));
				new Timer() {

					@Override
					public void run() {
						map.getInfoWindow().close();
					}
				}.schedule(INFO_WINDOW_AUTOCLOSE);
			}
		});

		map.addOverlay(marker);
		onScreenResults.add(earthquake);
		return marker;
	}

	/**
	 * @param earthquakes
	 * @return a {@link LatLng} whose coords are a mean of all the given earthquakes
	 */
	protected LatLng calculateMapCenter(INGVResult[] earthquakes) {
		double latitude = 0.0;
		double longitude = 0.0;
		for (INGVResult eq : earthquakes) {
			longitude += eq.getLongitude();
			latitude += eq.getLatitude();
		}
		return LatLng.newInstance(latitude / earthquakes.length, longitude
				/ earthquakes.length);
	}

	/**
	 * Since tweets are ordered by time (most recent first), if the first object
	 * of both the array and the list given as parameters represent the same tweet,
	 * there are no updates to show.
	 * @param earthquakes
	 * @param onScreenResults
	 * @return true if earthquakes[0] is the same as onScreenResults.get(0)
	 */
	protected boolean thereAreUpdates(INGVResult[] earthquakes,	List<INGVResult> onScreenResults) {
		if(earthquakes.length == 0) {
			return false;
		}
		if(onScreenResults.isEmpty()) { 
			return true;
		}
		if(earthquakes[0].equalsTo(onScreenResults.get(0))) {
			return false;
		} else {
			return true;
		}
	}
	
}
